/*
 -- IOTA Crypto Core
 --
 -- 2018 by Thomas Pototschnig <microengineer18@gmail.com>
 -- discord: pmaxuw#8292
 -- https://gitlab.com/iccfpga-rv
 --
 -- Permission is hereby granted, free of charge, to any person obtaining
 -- a copy of this software and associated documentation files (the
 -- "Software"), to deal in the Software without restriction, including
 -- without limitation the rights to use, copy, modify, merge, publish,
 -- distribute, sublicense, and/or sell copies of the Software, and to
 -- permit persons to whom the Software is furnished to do so, subject to
 -- the following conditions:
 --
 -- The above copyright notice and this permission notice shall be
 -- included in all copies or substantial portions of the Software.
 --
 -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 -- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 -- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 -- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 -- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 -- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 -- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWAR
 */


#include <unistd.h>
#include <ArduinoJson.h>
#include <stdint.h>

#include "main.h"

#include "micro-os-plus/diag/trace.h"
#include "debugprintf.h"

#include "secure/secure.h"
#include "secure/sbi.h"

#include "api/API.h"

#include "iota/transaction.h"
#include "iota/transfers.h"
#include "iota/bundle.h"

#include "utils.h"

extern API::apiFlagsStruct apiFlags;

#define min(a,b)	(a<b)?a:b

namespace API {
	// example
	// {"command":"setActiveSeed","path":"44'/4218'/0'/0'","security":2}
	int apiSetActiveSeedLC() {
		if (
			!exists<const char*>(json, "path") ||
			!exists<int>(json, "security")
		) {
			return apiError(SBIError::MISSING_OR_INVALID);
		}

		const char* path = json["path"];
		if (!path) {
			return apiError(SBIError::INVALID_BIP_PATH);
		}

		uint32_t uPath[4];
		if (!validateBIPPath(path, 4, uPath)) {
			return apiError(SBIError::INVALID_BIP_PATH);
		}

		int securityLevel = json["security"];
		if (securityLevel < 1 || securityLevel > 3) {
			return apiError(SBIError::INVALID_SECLEVEL);
		}

		SBIResponse* resp = SBI::setActiveSeedLC(uPath, securityLevel);
		if (resp->isError()) {
			return apiError(resp->code);
		}
		json.clear();
		return API_SUCCESS;
	}

	// example
	// {"command":"getAddress","keyIndex":0,"options":{"checksum":true,"display":true}}
	// {"command":"getAddress","keyIndex":0,"number":100, "options":{"checksum":true,"display":true}}
	// {"command":"getAddress","slot":0, "keyIndex":0,"options":{"checksum":true,"display":true}}
	// {"command":"getAddress","slot":0, "keyIndex":0,"number":100, "options":{"checksum":true,"display":true}}
	int apiGetAddressLC() {
		if (
			!exists<uint32_t>(json, "keyIndex") ||
			!optional<JsonObject>(json, "options") ||
			!optional<uint32_t>(json, "security") ||
			!optional<uint32_t>(json, "slot") ||
			!optional<uint32_t>(json, "number")
		) {
			return apiError(SBIError::MISSING_OR_INVALID);
		}

		uint32_t keyIndex = json["keyIndex"];
		if (keyIndex > 0xffffff00) {
			return apiError(SBIError::INVALID_KEY_INDEX);
		}

		bool checksum = false;
		bool display = false;

		JsonObject options = json["options"];
		if (!options.isNull()) {
			checksum = options["checksum"];	// false if key not present
			display = options["display"];		// false if key not present
		}

		uint32_t slot = json["slot"];
		if (!json.containsKey("slot")) {	// assume bip39 slot
			slot = 7;
		} else {
			if (slot > 7) {
				return apiError(SBIError::INVALID_SLOT);
			}
		}

		uint32_t number = json["number"];
		if (!json.containsKey("number")) {
			number = 1;
		} else {
			if (number < 1) {
				return apiError(SBIError::INVALID_COUNT);
			}
		}

		uint32_t security = json["security"];
		// security == 0 is valid because then the secureLev from SecureSeed is used
		if (security > 3) {
			return apiError(SBIError::INVALID_SECLEVEL);
		}

		json.clear();
		JsonArray addresses = json.createNestedArray("trytes");

		for (uint32_t index = keyIndex; index < keyIndex + number; index++) {
			GetAddressResponse* resp = SBI::getAddressLC(slot, security, index, checksum, display);
			if (resp->isError()) {
				return apiError(resp->code);
			}
			addresses.add(resp->address);
		}
		return API_SUCCESS;
	}

	/**
	   * Prepares the array of raw transaction data (trytes) by generating a bundle and signing the inputs.
	   *
	   * @param {Object[]} transfers - Transfer objects
	   * @param {String} transfers[].address - Tryte-encoded address of recipient, with or without the 9 tryte checksum
	   * @param {Integer} transfers[].value - Value to be transferred
	   * @param {String} transfers[].tag - Tryte-encoded tag. Maximum value is 27 trytes.g
	   * @param {Object[]} inputs - Inputs used for funding the transfer
	   * @param {String} inputs[].address - Tryte-encoded source address, with or without the 9 tryte checksum
	   * @param {Integer} inputs[].balance - Balance of that input
	   * @param {String} inputs[].keyIndex - Index of the address
	   * @param {Object} [remainder] - Destination for sending the remainder value (of the inputs) to.
	   * @param {String} remainder.address - Tryte-encoded address, with or without the 9 tryte checksum
	   * @param {Integer} remainder.keyIndex - Index of the address
	   * @param {Function} [now = Date.now()] - Function to get the milliseconds since the UNIX epoch for timestamps.
	   * @returns {Promise<String[]>} Transaction trytes of 2673 trytes per transaction
	   */

	// {"command":"prepareTransfers",
	// "transfers":	[{"address":"AOTJZ9QOYAF9RUMJXOUOZYX9MBMSVS9NKPBSCODCJSOHHKAVBG99MHHAYOXQLHQIHWNKHM9HQXXQAINAC","value":100}],
	// "inputs":[{"address":"AOTJZ9QOYAF9RUMJXOUOZYX9MBMSVS9NKPBSCODCJSOHHKAVBG99MHHAYOXQLHQIHWNKHM9HQXXQAINAC","keyIndex":0,"balance":1000}],
	// "remainder":{"address":"AOTJZ9QOYAF9RUMJXOUOZYX9MBMSVS9NKPBSCODCJSOHHKAVBG99MHHAYOXQLHQIHWNKHM9HQXXQAINAC","keyIndex":0},"timestamp":1500000000}
	namespace {
		Transaction* initTX(int idx) {
			if (idx < 0 || idx >= MAX_NUM_TRANSACTIONS) {
				return (Transaction*) 0;
			}
			txs[idx].init();
			return &txs[idx];
		}

		SBIError addInputOrRemainder(JsonObject& json, Transaction* tx, int64_t& total, bool isInput) {
			uint32_t keyIndex = json["keyIndex"];	// may be zero

			const char* address = json["address"];
			if (!validateTrytes(address, 81)) {
				return SBIError::INVALID_ADDRESS;
			}

			tx->setAddress(address);
			tx->setAddressIndex(keyIndex);

			if (isInput) {
				int64_t balance = json["balance"];
				if (balance < 0ll) {
					return SBIError::INVALID_VALUE;
				}
				tx->setValue(-balance);
				tx->setType(Transaction::Type::Input);

				total -= balance;
			} else {
				// is Remainder - give rest of tokens
				if (total > 0) { // insufficient
					return SBIError::INSUFFICIENT_TOKENS;
				}
				tx->setType(Transaction::Type::Remainder);
				tx->setValue(-total);
				total = 0;
			}
			return SBIError::NONE;
		}

		SBIError addInput(JsonObject& json, Transaction* tx, int64_t& total) {
			if (
				!exists<const char*>(json, "address") ||
				!exists<uint32_t>(json, "keyIndex") ||
				!exists<int64_t>(json, "balance")
			) {
				return SBIError::MISSING_OR_INVALID;
			}
			return addInputOrRemainder(json, tx, total, true);
		}

		SBIError addRemainder(JsonObject& json, Transaction *tx, int64_t& total) {
			if (
				!exists<const char*>(json, "address") ||
				!exists<uint32_t>(json, "keyIndex")
			) {
				return SBIError::MISSING_OR_INVALID;
			}
			return addInputOrRemainder(json, tx, total, false);
		}


		SBIError addOutput(JsonObject& transfer, Transaction *tx, int64_t& total) {
			if (
				!exists<const char*>(transfer, "address") ||
				!exists<int64_t>(transfer, "value") ||
				!optional<const char*>(transfer, "tag") ||
				!optional<const char*>(transfer, "message")
			) {
				return SBIError::INVALID_TRANSFERS;
			}

			const char* address = transfer["address"];
			if (!validateTrytes(address, 81)) {
				return SBIError::INVALID_ADDRESS;
			}

			const char* tag = transfer["tag"];
			if (tag) {
				if (!validateTrytesMaxLen(tag, 27)) {
					return SBIError::INVALID_TAG;
				}
				tx->setTag(tag);
				if (strlen(tag) < 27) {
					tx->getTag()[strlen(tag)] = 0; // zero terminate for verification
				}
			}

			const char* message = transfer["message"];
			if (message) {
				if (!validateTrytesMaxLen(message, 2187)) {
					return SBIError::INVALID_MESSAGE;
				}
				tx->setMessage(message);
				if (strlen(message) < 2187) {
					tx->getsignatureMessageFragment()[strlen(message)] = 0;	// zero terminate for verification
				}
			}

			int64_t value = transfer["value"];
			if (value < 0ll) {
				return SBIError::INVALID_VALUE;
			}
			tx->setType(Transaction::Type::Output);
			tx->setValue(value);
			tx->setAddress(address);

			total += value;

			return SBIError::NONE;
		}
	}

	int apiPrepareTransfersLC() {
		// exists and optional checks for valid json arrays/objects
		if (
			!exists<JsonArray>(json, "transfers") ||
			!exists<JsonArray>(json, "inputs") ||
			!exists<int64_t>(json, "timestamp") ||
			!exists<uint32_t>(json, "slot") ||
			!optional<JsonObject>(json, "remainder") ||
			!optional<const char*>(json, "auth")
		) {
			return apiError(SBIError::MISSING_OR_INVALID);
		}

		JsonArray transfersArray = json["transfers"];

		bool hasInputs = json.containsKey("inputs");
		bool hasRemainder = json.containsKey("remainder");


		uint32_t timestamp = json["timestamp"];

		uint32_t slot = json["slot"];
		if (slot > 7) {
			return apiError(SBIError::INVALID_SLOT);
		}

		const char* auth = json["auth"];
		uint8_t authBytes[48]={0};
		if (apiFlags.authMethod == API::AUTH) {
			if (!validateHex(auth, 96))
				return apiError(SBIError::INVALID_AUTH);

			if (!hexToBytes(auth, authBytes, sizeof(authBytes))) {
				return SBIError::INVALID_AUTH;
			}
		}

		int64_t total = 0ll;
		int idx = 0;

		// size check will be done in SBI call because activeSeedSecurityLevel is protected
		for (size_t i=0;i<transfersArray.size(); i++) {
			Transaction* tx = initTX(idx++);
			if (!tx) {
				return apiError(SBIError::TOO_MANY_TX);
			}

			JsonObject transfer = transfersArray[i];
			SBIError err = addOutput(transfer, tx, total);
			if (err != SBIError::NONE) {
				return apiError(err);
			}
		}

		if (hasInputs) {
			JsonArray inputsArray = json["inputs"];

			for (size_t i=0;i<inputsArray.size(); i++) {
				Transaction* tx = initTX(idx++);
				if (!tx) {
					return apiError(SBIError::TOO_MANY_TX);
				}

				JsonObject input = inputsArray[i];
				SBIError err = addInput(input, tx, total);
				if (err != SBIError::NONE) {
					return apiError(err);
				}
			}
		}
		if (hasRemainder && total < 0) {	// if there are remaining coins
			JsonObject remainder = json["remainder"]; // todo string should be changeAddress?

			Transaction* tx = initTX(idx++);
			if (!tx) {
				return apiError(SBIError::TOO_MANY_TX);
			}

			SBIError err = addRemainder(remainder, tx, total);
			if (err != SBIError::NONE) {
				return apiError(err);
			}
		}

		if (total > 0) { // insuficcient
			return apiError(SBIError::INSUFFICIENT_TOKENS);
		} else if (total < 0) { // unspent
			return apiError(SBIError::UNSPENT_TOKENS);
		}


		// now we have built transactions - call prepareTransfer
		// auth may be zero
		PrepareTransfersResponse* resp = SBI::prepareTransfersLC(timestamp, slot, 0, idx, authBytes);
		if (resp->isError()) {
			return apiError(resp->code);
		}

		// if trunk, branch and mwm is given, attachToTangle the TXs
		// reduces round-trip-time of sending transactions back and forth
		if (
			exists<const char*>(json, "trunkTransaction") &&
			exists<const char*>(json, "branchTransaction") &&
			exists<uint32_t>(json, "minWeightMagnitude")
		) {
			const char* trunkTransaction = json["trunkTransaction"];
			if (!validateTrytes(trunkTransaction, 81)) {
				return apiError(SBIError::INVALID_TRUNK);
			}

			const char* branchTransaction = json["branchTransaction"];
			if (!validateTrytes(branchTransaction, 81)) {
				return apiError(SBIError::INVALID_BRANCH);
			}

			uint32_t mwm = json["minWeightMagnitude"];
			if (mwm < 3 || mwm > 18) {
				return apiError(SBIError::INVALID_MWM);
			}

			if (!attachToTangle(resp->first, trunkTransaction, branchTransaction, mwm, (int64_t) timestamp * 1000ll, (char*) 0)) {
				return apiError(SBIError::ERROR);
			}
		}

		json.clear();
		JsonArray trytesOut = json.createNestedArray("trytes");

		Transaction* tx = resp->first;
		if (!tx) {
			return apiError(SBIError::ERROR);
		}

		json["bundleHash"] = resp->bundleHash;
		// we can rely on the chain being terminated properly
		do {
			trytesOut.add(tx->getRawPtr());	// is zero-terminated, so this is fine
			tx = tx->getNext();
		} while (tx);
		return API_SUCCESS;
	}

	int apiGetAppVersionLC() {
		return API_SUCCESS;
	}

	int apiGetAppMaxBundleSizeLC() {
		return API_SUCCESS;
	}

	bool registerAPIWallet() {
		return
				registerAPICall("setActiveSeed", &apiSetActiveSeedLC) &&
				registerAPICall("getAddress", &apiGetAddressLC) &&
				registerAPICall("prepareTransfers", &apiPrepareTransfersLC) &&
				registerAPICall("getAppVersion", &apiGetAppVersionLC) &&
				registerAPICall("getAppMaxBundleSize", &apiGetAppMaxBundleSizeLC);
	}

}

